﻿
using S7Cat.Enums;
using S7Cat.Models;
using S7Cat.Tools;
using System.Buffers.Binary;
using System.Runtime.CompilerServices;

namespace S7Cat.Protocol
{
    internal static class PackageParser
    {
        private const int PduErrOffset = 20;
        private const int UserDataResultOffset = PduErrOffset + 2;
        private const int DateTimeLength = 10;
        /// <summary>
        /// The minimum <see cref="T:System.DateTime"/> value supported by the specification.
        /// </summary>
        public static readonly System.DateTime SpecMinimumDateTime = new(1990, 1, 1);

        /// <summary>
        /// The maximum <see cref="T:System.DateTime"/> value supported by the specification.
        /// </summary>
        public static readonly System.DateTime SpecMaximumDateTime = new(2089, 12, 31, 23, 59, 59, 999);


        public static void ValidateResponseCode(ReadWriteErrorCode statusCode)
        {
            switch (statusCode)
            {
                case ReadWriteErrorCode.ObjectDoesNotExist:
                    throw new Exception("Received error from PLC: Object does not exist.");
                case ReadWriteErrorCode.DataTypeInconsistent:
                    throw new Exception("Received error from PLC: Data type inconsistent.");
                case ReadWriteErrorCode.DataTypeNotSupported:
                    throw new Exception("Received error from PLC: Data type not supported.");
                case ReadWriteErrorCode.AccessingObjectNotAllowed:
                    throw new Exception("Received error from PLC: Accessing object not allowed.");
                case ReadWriteErrorCode.AddressOutOfRange:
                    throw new Exception("Received error from PLC: Address out of range.");
                case ReadWriteErrorCode.HardwareFault:
                    throw new Exception("Received error from PLC: Hardware fault.");
                case ReadWriteErrorCode.Success:
                    break;
                default:
                    throw new Exception($"Invalid response from PLC: statusCode={(byte)statusCode}.");
            }
        }

        //public static void FillReadResponseIntoDataItem(Span<byte> s7data, DataItemAddress item)
        //{
        //    if(item.Data == null || item.Data.Length != item.ByteCount)
        //    {
        //        item.Data = new byte[item.ByteCount];
        //    }

        //    s7data.Slice(18, item.ByteCount).CopyTo(item.Data);            
        //}

        public static void FillReadResponses(Span<byte> s7data, Span<DataItemAddress> items, Span<byte> buffer)
        {
            int offset = 14;
            var bufferOffset = 0;

            for (int i = 0; i < items.Length; i++)
            {
                // check for Return Code = Success
                if (s7data[offset] != 0xff)
                    throw new Exception("PLC communication failed with error: WrongNumberReceivedBytes");

                offset += 4;

                //s7data.Slice(offset, items[i].ByteCount).CopyTo(results[i]);
                s7data.Slice(offset, items[i].ByteCount).CopyTo(buffer.Slice(bufferOffset, items[i].ByteCount));

                offset += items[i].ByteCount;

                // Always align to even offset
                if (offset % 2 != 0)
                    offset++;

                bufferOffset += items[i].ByteCount;
            }
        }


        public static void ValidateWriteResponses(Span<byte> s7data, Span<DataItemAddress> items)
        {
            if (s7data.Length < 12) throw new Exception("Not enough data received to parse write response.");

            var messageError = BinaryPrimitives.ReadUInt16BigEndian(s7data.Slice(10, 2));
            if (messageError != 0)
                throw new Exception($"Write failed with error {messageError}.");

            if (s7data.Length < 14 + items.Length)
                throw new Exception("Not enough data received to parse individual item responses.");

            List<Exception>? list = null;
            var results = s7data.Slice(14);
            for (int i = 0; i < items.Length; i++)
            {
                var expection = GetExpectionFromResponseCode((ReadWriteErrorCode)results[i], items[i]);
                if (expection != null)
                {
                    list ??= [];
                    list.Add(expection);
                }
            }
            if (list != null)
            {
                throw new AggregateException($"Write failed for {list.Count} items. See the innerExceptions for details.\r\n", list);
            }
        }

        public static void ValidateWriteResponse(Span<byte> s7data, DataItemAddress item)
        {
            if (s7data.Length < 12) throw new Exception("Not enough data received to parse write response.");

            var messageError = BinaryPrimitives.ReadUInt16BigEndian(s7data.Slice(10, 2));
            if (messageError != 0)
                throw new Exception($"Write failed with error {messageError}.");

            if (s7data.Length < 15)
                throw new Exception("Not enough data received to parse individual item responses.");

            var expection = GetExpectionFromResponseCode((ReadWriteErrorCode)s7data[14], item);
            if (expection != null)
            {
                throw expection;
            }
        }

        private static Exception? GetExpectionFromResponseCode(ReadWriteErrorCode statusCode, DataItemAddress dataItem)
        {
            return statusCode switch
            {
                ReadWriteErrorCode.ObjectDoesNotExist =>
                    new Exception($"{dataItem.ToCode()} Write failed: Object does not exist."),
                ReadWriteErrorCode.DataTypeInconsistent =>
                    new Exception($"{dataItem.ToCode()} Write failed: Data type inconsistent."),
                ReadWriteErrorCode.DataTypeNotSupported =>
                    new Exception($"{dataItem.ToCode()} Write failed: Data type not supported."),
                ReadWriteErrorCode.AccessingObjectNotAllowed =>
                    new Exception($"{dataItem.ToCode()} Write failed: Accessing object not allowed"),
                ReadWriteErrorCode.AddressOutOfRange =>
                    new Exception($"{dataItem.ToCode()} Write failed: Address out of range."),
                ReadWriteErrorCode.HardwareFault =>
                    new Exception($"{dataItem.ToCode()} Write failed: Hardware fault."),
                ReadWriteErrorCode.Success =>
                    null,
                _ =>
                    new Exception($"{dataItem.ToCode()} Write failed: statusCode={(byte)statusCode}."),
            };
        }


        public static DateTime ParseReadClockResponse(ReadOnlySpan<byte> s7data)
        {
            const int udLenOffset = UserDataResultOffset + 2;
            const int udValueOffset = udLenOffset + 2;
            const int dateTimeSkip = 2;

            AssertPduResult(s7data);
            AssertUserDataResult(s7data, 0xff);

            var len = BinaryPrimitives.ReadInt16BigEndian(s7data.Slice(udLenOffset, 2));

            return len != DateTimeLength
                ? throw new Exception($"Unexpected response length {len}, expected {DateTimeLength}.")
                : BytesToDateTime(s7data.Slice(udValueOffset + dateTimeSkip, DateTimeLength - dateTimeSkip));
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void AssertPduResult(ReadOnlySpan<byte> s7data)
        {
            const int PduErrOffset = 20;

            var pduErr = BinaryPrimitives.ReadInt16BigEndian(s7data.Slice(PduErrOffset, 2));
            if (pduErr != 0)
            {
                throw new Exception($"Response from PLC indicates error 0x{pduErr:X4}.");
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static void AssertUserDataResult(ReadOnlySpan<byte> s7data, byte expected)
        {
            var dtResult = s7data[UserDataResultOffset];
            if (dtResult != expected)
            {
                throw new Exception($"Response from PLC was 0x{dtResult:X2}, expected 0x{expected:X2}.");
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static DateTime BytesToDateTime(ReadOnlySpan<byte> bytes)
        {
            if (bytes.Length != 8)
                throw new ArgumentOutOfRangeException(nameof(bytes), bytes.Length,
                    $"Parsing a DateTime requires exactly 8 bytes of input data, input data is {bytes.Length} bytes long.");

            var year = ByteToYear(bytes[0]);
            var month = AssertRangeInclusive(DecodeBcd(bytes[1]), 1, 12, "month");
            var day = AssertRangeInclusive(DecodeBcd(bytes[2]), 1, 31, "day of month");
            var hour = AssertRangeInclusive(DecodeBcd(bytes[3]), 0, 23, "hour");
            var minute = AssertRangeInclusive(DecodeBcd(bytes[4]), 0, 59, "minute");
            var second = AssertRangeInclusive(DecodeBcd(bytes[5]), 0, 59, "second");
            var hsec = AssertRangeInclusive(DecodeBcd(bytes[6]), 0, 99, "first two millisecond digits");
            var msec = AssertRangeInclusive(bytes[7] >> 4, 0, 9, "third millisecond digit");
            //var dayOfWeek = AssertRangeInclusive(bytes[7] & 0b00001111, 1, 7, "day of week");

            return new System.DateTime(year, month, day, hour, minute, second, hsec * 10 + msec);

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            int DecodeBcd(byte input)
            {
                return 10 * (input >> 4) + (input & 0b00001111);
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            int ByteToYear(byte bcdYear)
            {
                var input = DecodeBcd(bcdYear);
                return input < 90
                    ? input + 2000
                    : input < 100
                    ? input + 1900
                    : throw new ArgumentOutOfRangeException(nameof(bcdYear), bcdYear,
                    $"Value '{input}' is higher than the maximum '99' of S7 date and time representation.");
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            int AssertRangeInclusive(int input, byte min, byte max, string field)
            {
                return input < min
                    ? throw new ArgumentOutOfRangeException(nameof(input), input,
                        $"Value '{input}' is lower than the minimum '{min}' allowed for {field}.")
                    : input > max
                    ? throw new ArgumentOutOfRangeException(nameof(input), input,
                        $"Value '{input}' is higher than the maximum '{max}' allowed for {field}.")
                    : input;
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static ArrayPoolBufferWriter<byte> DateTimeToBytes(DateTime dateTime)
        {
            if (dateTime < SpecMinimumDateTime)
                throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime,
                    $"Date time '{dateTime}' is before the minimum '{SpecMinimumDateTime}' supported in S7 date time representation.");

            if (dateTime > SpecMaximumDateTime)
                throw new ArgumentOutOfRangeException(nameof(dateTime), dateTime,
                    $"Date time '{dateTime}' is after the maximum '{SpecMaximumDateTime}' supported in S7 date time representation.");

            var buffer = new ArrayPoolBufferWriter<byte>(8);

            buffer.Write(EncodeBcd(MapYear(dateTime.Year)));
            buffer.Write(EncodeBcd(dateTime.Month));
            buffer.Write(EncodeBcd(dateTime.Day));
            buffer.Write(EncodeBcd(dateTime.Hour));
            buffer.Write(EncodeBcd(dateTime.Minute));
            buffer.Write(EncodeBcd(dateTime.Second));
            buffer.Write(EncodeBcd(dateTime.Millisecond / 10));
            buffer.Write((byte)(dateTime.Millisecond % 10 << 4 | DayOfWeekToInt(dateTime.DayOfWeek)));

            return buffer;

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            byte EncodeBcd(int value)
            {
                return (byte)((value / 10 << 4) | value % 10);
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            byte MapYear(int year)
            {
                return (byte)(year < 2000 ? year - 1900 : year - 2000);
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            int DayOfWeekToInt(DayOfWeek dayOfWeek)
            {
                return (int)dayOfWeek + 1;
            }
        }
    }
}
